package ru.evga.inse.core.rpc.json;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.ApplicationContext;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.support.RequestContextUtils;

import ru.evga.inse.core.exception.InfoseaException;
import ru.evga.inse.core.exception.UnlocaliseMessage;
import ru.evga.inse.core.rpc.ReflectionUtil;
import ru.evga.inse.core.rpc.RpcParamName;


/**
 * A JSON-RPC request server reads JSON-RPC requests from an 
 * input stream and writes responses to an output stream.
 */
@Configurable
public class JsonRpcServer
{
    static Logger logger = Logger.getLogger(JsonRpcServer.class);
    
    private ObjectMapper m_mapper;
    private Object m_handler;
    private Class<?> m_remoteInterface;
    @Autowired
    private ApplicationContext m_applicationContext;

    /**
     * Creates the server with the given {@link ObjectMapper} delegating
     * all calls to the given {@code handler} {@link Object} but only
     * methods available on the {@code remoteInterface}.
     * @param mapper the {@link ObjectMapper}
     * @param handler the {@code handler}
     * @param remoteInterface the interface
     */
    public JsonRpcServer(ObjectMapper mapper, Object handler, Class<?> remoteInterface)
    {
        this.m_mapper = mapper;
        this.m_handler = handler;
        this.m_remoteInterface = remoteInterface;
    }

    /**
     * Creates the server with the given {@link ObjectMapper} delegating
     * all calls to the given {@code handler}.
     * @param mapper the {@link ObjectMapper}
     * @param handler the {@code handler}
     */
    public JsonRpcServer(ObjectMapper mapper, Object handler)
    {
        this(mapper, handler, null);
    }

    /**
     * Creates the server with a default {@link ObjectMapper} delegating
     * all calls to the given {@code handler} {@link Object} but only
     * methods available on the {@code remoteInterface}.
     * @param handler the {@code handler}
     * @param remoteInterface the interface
     */
    public JsonRpcServer(Object handler, Class<?> remoteInterface)
    {
        this(new ObjectMapper(), handler, remoteInterface);
    }

    /**
     * Creates the server with a default {@link ObjectMapper} delegating
     * all calls to the given {@code handler}.
     * @param handler the {@code handler}
     */
    public JsonRpcServer(Object handler)
    {
        this(new ObjectMapper(), handler, null);
    }

    /**
     * Handles a single request from the given {@link InputStream},
     * that is to say that a single {@link JsonNode} is read from
     * the stream and treated as a JSON-RPC request.  All responses
     * are written to the given {@link OutputStream}.
     * @param ips the {@link InputStream}
     * @param ops the {@link OutputStream}
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     */
    public void handle(InputStream ips, OutputStream ops, HttpServletRequest request)
        throws JsonParseException, JsonMappingException, IOException
    {
        handleNode(m_mapper.readValue(ips, JsonNode.class), ops, request);
    }

    /**
     * Returns the handler's class or interface.
     * @return the class
     */
    private Class<?> getHandlerClass()
    {
        return (m_remoteInterface != null) ? m_remoteInterface : m_handler.getClass();
    }

    /**
     * Handles the given {@link JsonNode} and writes the
     * responses to the given {@link OutputStream}.
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws JsonGenerationException
     * @throws JsonMappingException
     * @throws IOException
     */
    private void handleNode(JsonNode node, OutputStream ops, HttpServletRequest request)
        throws JsonGenerationException, JsonMappingException, IOException
    {
        // handle objects
        if (node.isObject())
        {
            handleObject(ObjectNode.class.cast(node), ops, request);
            // handle arrays
        }
        else if (node.isArray())
        {
            handleArray(ArrayNode.class.cast(node), ops, request);
            // bail on bad data
        }
        else
        {
            throw new IllegalArgumentException("Invalid JsonNode type: "
                + node.getClass().getName());
        }
    }

    /**
     * Handles the given {@link ArrayNode} and writes the
     * responses to the given {@link OutputStream}.
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws JsonGenerationException
     * @throws JsonMappingException
     * @throws IOException
     */
    private void handleArray(ArrayNode node, OutputStream ops, HttpServletRequest request)
        throws JsonGenerationException, JsonMappingException, IOException
    {
        // loop through each array element
        for (int i = 0; i < node.size(); i++)
        {
            handleNode(node.get(i), ops, request);
        }
    }

    /**
     * Handles the given {@link ObjectNode} and writes the
     * responses to the given {@link OutputStream}.
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws JsonGenerationException
     * @throws JsonMappingException
     * @throws IOException
     */
    private void handleObject(ObjectNode node, OutputStream ops, HttpServletRequest request)
        throws JsonGenerationException, JsonMappingException, IOException
    {
        // validate request
        if (!node.has("jsonrpc") || !node.has("method"))
        {
            m_mapper.writeValue(ops,
                createErrorResponse("jsonrpc", "null", -32600, "Invalid Request", null));
            return;
        }
        // parse request
        String jsonRpc = node.get("jsonrpc").getValueAsText();
        String methodName = node.get("method").getValueAsText();
        String id = node.get("id").getValueAsText();
        JsonNode params = node.get("params");
        
        if(logger.isDebugEnabled())
        {
            logger.debug("Json command: " + node.toString());
        }
        
//        int paramCount = (params != null) ? params.size() : 0;
        // find methods
        Set<Method> methods = ReflectionUtil.findMethods(getHandlerClass(), methodName);
        // method not found
        if (methods.isEmpty())
        {
            m_mapper.writeValue(ops,
                createErrorResponse(jsonRpc, id, -32601, "Method not found", null));
            return;
        }
        else if (methods.size() > 1)
        {
            m_mapper.writeValue(ops,
                createErrorResponse(jsonRpc, id, -32601, "Found more one method", null));
        }
        
        Method method = methods.iterator().next();
        
        List<JsonNode> paramNodes = new ArrayList<JsonNode>();
        
        // get method annotations
        Annotation [][] annotations = method.getParameterAnnotations();
        boolean found = true;
        for (int i = 0; i < annotations.length; i++)
        {
            // look for param name annotations
            String paramName = null;
            for (int j = 0; j < annotations[i].length; j++)
            {
                if (!RpcParamName.class.isInstance(annotations[i][j]))
                {
                    continue;
                }
                else
                {
                    paramName = RpcParamName.class.cast(annotations[i][j]).value();
                    continue;
                }
            }
            // bail if param name wasn't found
            if (params.has(paramName))
            {
                paramNodes.add(params.get(paramName));
            }
            else
            {
                paramNodes.add(null);
            }
        }

                
//        // invalid parameters
//        if (method == null)
//        {
//            m_mapper.writeValue(ops,
//                createErrorResponse(jsonRpc, id, -32602, "Invalid method parameters.", null));
//            return;
//        }
        // invoke the method
        JsonNode result = null;
        ObjectNode error = null;
        String errorMessage = null;
        try
        {
            result = invoke(method, paramNodes);
        }
        catch (Throwable e)
        {
            if (InvocationTargetException.class.isInstance(e))
            {
                e = InvocationTargetException.class.cast(e).getTargetException();
            }
            if (e instanceof InfoseaException)
            {
                UnlocaliseMessage unlocaliseMessage = ((InfoseaException)e).getUnlocaliseMessage();
                LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
                errorMessage = m_applicationContext.getMessage(unlocaliseMessage.getMessageId(),
                    unlocaliseMessage.getArguments(), unlocaliseMessage.getMessageId(),
                    localeResolver.resolveLocale(request));
            }
            else
            {
                errorMessage = e.getLocalizedMessage();
            }
            //			error.put("data", e.getClass().getName());
        }
        if (errorMessage != null)
        {
            error = m_mapper.createObjectNode();
            error.put("code", 1);
            error.put("message", errorMessage);
        }
        // bail if notification request
        if (id == null)
        {
            return;
        }
        // create response
        ObjectNode response = m_mapper.createObjectNode();
        response.put("jsonrpc", jsonRpc);
        response.put("id", id);
        if (error == null)
        {
            response.put("result", result);
        }
        else if (error != null)
        {
            response.put("error", error);
        }
        // write it
        m_mapper.writeValue(ops, response);
    }

    /**
     * Invokes the given method on the {@code handler} passing
     * the given params (after converting them to beans\objects)
     * to it.
     * @param m the method to invoke
     * @param params the params to pass to the method
     * @return the return value (or null if no return)
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private JsonNode invoke(Method m, List<JsonNode> params)
        throws JsonParseException, JsonMappingException, IOException, IllegalArgumentException,
        IllegalAccessException, InvocationTargetException
    {
        // convert the parameters
        Object [] convertedParams = new Object[params.size()];
        Type [] parameterTypes = m.getGenericParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++)
        {
            JsonNode param = params.get(i);
            if (param == null)
            {
                convertedParams[i] = null;
            }
            else
            {
            convertedParams[i] = m_mapper.treeToValue(param,
                TypeFactory.type(parameterTypes[i]).getRawClass());
            }
        }
        // invoke the method
        Object result = m.invoke(m_handler, convertedParams);
        return (m.getGenericReturnType() != null) ? m_mapper.valueToTree(result) : null;
    }

    /**
     * Convenience method for creating an error response
     * @param jsonRpc the jsonrpc string
     * @param id the id
     * @param code the error code
     * @param message the error message
     * @param data the error data (if any)
     * @return the error response
     */
    private ObjectNode createErrorResponse(String jsonRpc, String id, int code, String message,
        Object data)
    {
        ObjectNode response = m_mapper.createObjectNode();
        ObjectNode error = m_mapper.createObjectNode();
        error.put("code", code);
        error.put("message", message);
        if (data != null)
        {
            error.put("data", m_mapper.valueToTree(data));
        }
        response.put("jsonrpc", jsonRpc);
        response.put("id", id);
        response.put("error", error);
        return response;
    }
}
